2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import <Adium/AIEmoticon.h>
18 #import <Adium/AIEmoticonPack.h>
19 #import <Adium/AIEmoticonControllerProtocol.h>
20 #import <AIUtilities/AIFileManagerAdditions.h>
21 #import <AIUtilities/AIImageAdditions.h>
23 #define EMOTICON_PATH_EXTENSION @"emoticon"
24 #define EMOTICON_PACK_TEMP_EXTENSION @"AdiumEmoticonOld"
26 #define EMOTICON_PLIST_FILENAME @"Emoticons.plist"
27 #define EMOTICON_PACK_VERSION @"AdiumSetVersion"
28 #define EMOTICON_LIST @"Emoticons"
30 #define EMOTICON_EQUIVALENTS @"Equivalents"
31 #define EMOTICON_NAME @"Name"
33 #define EMOTICON_SERVICE_CLASS @"Service Class"
35 #define EMOTICON_LOCATION @"Location"
36 #define EMOTICON_LOCATION_SEPARATOR @"////"
38 @interface AIEmoticonPack (PRIVATE)
39 - (AIEmoticonPack *)initFromPath:(NSString *)inPath;
40 - (void)setEmoticonArray:(NSArray *)inArray;
41 - (void)loadEmoticons;
42 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict;
43 - (void)loadProteusEmoticons:(NSDictionary *)emoticons;
44 - (void)_upgradeEmoticonPack:(NSString *)packPath;
45 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath;
46 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath;
47 - (NSString *)_stringWithMacEndlines:(NSString *)inString;
52 * @class AIEmoticonPack
53 * @brief Class to encapsulate an emoticon pack, which is a themed collection of emoticons
55 * An emoticon pack must have a name and a set of one or more emoticons (AIEmoticon objects).
56 * It may also have a serviceClass, which indicates the class of a service upon which its emoticons are preferred.
57 * For example, a set of MSN emoticons would have a service class of @"MSN".
59 @implementation AIEmoticonPack
62 * @brief Create a new emoticon pack
63 * @param inPath The path to the root of a bundle of emoticons
65 + (id)emoticonPackFromPath:(NSString *)inPath
67 return [[[self alloc] initFromPath:inPath] autorelease];
71 - (AIEmoticonPack *)initFromPath:(NSString *)inPath
73 if ((self = [super init])) {
74 path = [inPath retain];
76 bundle = [[NSBundle bundleWithPath:path] retain];
79 if (xtraBundle && ([[xtraBundle objectForInfoDictionaryKey:@"XtraBundleVersion"] intValue] == 1)) {
80 //This checks for a new-style xtra
81 //New style xtras store the same info, but it's in Contents/Resources/ so that we can have an info.plist file and use NSBundle.
82 emoticonLocation = [[xtraBundle resourcePath] retain];
86 NSString *localizedName;
87 name = [[path lastPathComponent] stringByDeletingPathExtension];
88 if ((localizedName = [[bundle localizedInfoDictionary] objectForKey:name])) {
94 enabledEmoticonArray = nil;
108 [emoticonArray release];
109 [enabledEmoticonArray release];
110 [serviceClass release];
116 * @brief Name, for display to the user
124 * @brief Path to this emoticon pack
132 * @brief Service class of this emoticon pack
134 * @result A service class, or nil if the emoticon pack is not associated with any service class
136 - (NSString *)serviceClass
142 * @brief An array of AIEmoticon objects
144 - (NSArray *)emoticons
146 if (!emoticonArray) [self loadEmoticons];
147 return emoticonArray;
151 * @brief An array of enabled AIEmoticon objects
153 - (NSArray *)enabledEmoticons
155 NSEnumerator *enumerator;
158 if (!enabledEmoticonArray) {
159 enabledEmoticonArray = [[NSMutableArray alloc] init];
160 enumerator = [[self emoticons] objectEnumerator];
161 while ((emo = [enumerator nextObject])) {
163 [enabledEmoticonArray addObject:emo];
167 return enabledEmoticonArray;
171 * @brief Return the preview image to use within a menu for this emoticon
173 * It tries to be the emoticon for text equivalent :) or :-). Failing that, any emoticon will do.
175 - (NSImage *)menuPreviewImage
177 NSArray *myEmoticons = [self emoticons];
178 NSEnumerator *enumerator;
179 AIEmoticon *emoticon;
181 enumerator = [myEmoticons objectEnumerator];
182 while ((emoticon = [enumerator nextObject])) {
183 NSArray *equivalents = [emoticon textEquivalents];
184 if ([equivalents containsObject:@":)"] || [equivalents containsObject:@":-)"]) {
189 //If we didn't find a happy emoticon, use the first one in the array
190 if (!emoticon && [myEmoticons count]) {
191 emoticon = [myEmoticons objectAtIndex:0];
194 return [[emoticon image] imageByScalingForMenuItem];
198 * @brief Set the emoticons that are disabled in this pack
199 * @param inArray An NSArray of AIEmoticon objects to disable
201 - (void)setDisabledEmoticons:(NSArray *)inArray
203 NSEnumerator *enumerator;
204 AIEmoticon *emoticon;
206 //Flag our emoticons as enabled/disabled
207 enumerator = [[self emoticons] objectEnumerator];
208 while ((emoticon = [enumerator nextObject])) {
209 [emoticon setEnabled:(![inArray containsObject:[emoticon name]])];
212 //reset the emabled emoticon list
213 if (enabledEmoticonArray) {
214 [enabledEmoticonArray release];
215 enabledEmoticonArray = nil;
220 * @brief Enable/Disable this pack
221 * @param inEnabled Should this pack be enabled?
223 - (void)setIsEnabled:(BOOL)inEnabled
229 * @brief Is this pack enabled?
235 //Copying --------------------------------------------------------------------------------------------------------------
238 - (id)copyWithZone:(NSZone *)zone
240 AIEmoticonPack *newPack = [[AIEmoticonPack alloc] initFromPath:path];
242 newPack->emoticonArray = [emoticonArray mutableCopy];
243 newPack->serviceClass = [serviceClass retain];
244 newPack->path = [path retain];
245 newPack->bundle = [bundle retain];
246 newPack->name = [name retain];
251 //Loading Emoticons ----------------------------------------------------------------------------------------------------
252 #pragma mark Loading Emoticons
254 * @brief Load the emoticons in this pack.
256 * Called by [self emoticons] as needed
258 - (void)loadEmoticons
260 [emoticonArray release]; emoticonArray = [[NSMutableArray alloc] init];
261 [serviceClass release]; serviceClass = nil;
264 NSString *infoDictPath = [bundle pathForResource:EMOTICON_PLIST_FILENAME ofType:nil];
265 NSDictionary *infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
266 NSDictionary *localizedInfoDict = [bundle localizedInfoDictionary];
268 //If no info dict was found, assume that this is an old emoticon pack and try to upgrade it
270 AILog(@"Upgrading Emoticon Pack %@ at %@...", self, bundle);
271 [self _upgradeEmoticonPack:path];
272 infoDict = [NSDictionary dictionaryWithContentsOfFile:infoDictPath];
273 [bundle release]; bundle = [[NSBundle bundleWithPath:path] retain];
278 /* Handle optional location key, which allows emoticons to be loaded
279 * from arbitrary directories. This is only used by the iChat emoticon
282 id possiblePaths = [infoDict objectForKey:EMOTICON_LOCATION];
284 if ([possiblePaths isKindOfClass:[NSString class]]) {
285 possiblePaths = [NSArray arrayWithObjects:possiblePaths, nil];
288 NSEnumerator *pathEnumerator = [possiblePaths objectEnumerator];
291 while ((aPath = [pathEnumerator nextObject])) {
292 NSString *possiblePath;
293 NSArray *splitPath = [aPath componentsSeparatedByString:EMOTICON_LOCATION_SEPARATOR];
295 /* Two possible formats:
297 * <string>/absolute/path/to/directory</string>
298 * <string>CFBundleIdentifier////relative/path/from/bundle/to/directory</string>
300 * The separator in the latter is ////, defined as EMOTICON_LOCATION_SEPARATOR.
302 if ([splitPath count] == 1) {
303 possiblePath = [splitPath objectAtIndex:0];
305 NSArray *components = [NSArray arrayWithObjects:
306 [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:[splitPath objectAtIndex:0]],
307 [splitPath objectAtIndex:1],
309 possiblePath = [NSString pathWithComponents:components];
312 /* If the directory exists, then we've found the location. If we
313 * make it all the way through the list without finding a valid
314 * directory, then the standard location will be used.
317 if ([[NSFileManager defaultManager] fileExistsAtPath:possiblePath isDirectory:&isDir] && isDir) {
319 bundle = [[NSBundle bundleWithPath:possiblePath] retain];
325 int version = [[infoDict objectForKey:EMOTICON_PACK_VERSION] intValue];
328 case 0: [self loadProteusEmoticons:infoDict]; break;
329 case 1: [self loadAdiumEmoticons:[infoDict objectForKey:EMOTICON_LIST] localizedStrings:localizedInfoDict]; break;
333 serviceClass = [[infoDict objectForKey:EMOTICON_SERVICE_CLASS] retain];
335 if ([name rangeOfString:@"AIM"].location != NSNotFound) {
336 serviceClass = [@"AIM-compatible" retain];
337 } else if ([name rangeOfString:@"MSN"].location != NSNotFound) {
338 serviceClass = [@"MSN" retain];
339 } else if ([name rangeOfString:@"Yahoo"].location != NSNotFound) {
340 serviceClass = [@"Yahoo!" retain];
345 //Sort the emoticons in this pack using the AIEmoticon compare: selector
346 [emoticonArray sortUsingSelector:@selector(compare:)];
350 * @brief Load an Adium version 1 emoticon pack
352 * @param emoticons A dictionary whose keys are file names and objects are themselves dictionaries with equivalent and name information.
354 - (void)loadAdiumEmoticons:(NSDictionary *)emoticons localizedStrings:(NSDictionary *)localizationDict
356 NSEnumerator *enumerator = [emoticons keyEnumerator];
358 NSBundle *myBundle = (!localizationDict ? [NSBundle bundleForClass:[self class]] : nil);
360 while ((fileName = [enumerator nextObject])) {
361 id dict = [emoticons objectForKey:fileName];
363 if ([dict isKindOfClass:[NSDictionary class]]) {
364 NSString *emoticonName = [(NSDictionary *)dict objectForKey:EMOTICON_NAME];
365 NSString *localizedEmoticonName = nil;
368 if (localizationDict) {
369 //If the bundle provides localizations, use them
370 localizedEmoticonName = [localizationDict objectForKey:emoticonName];
373 if (!localizedEmoticonName) {
374 //Otherwise, look at our list of default translations (generated at the bottom of this file)
375 localizedEmoticonName = [myBundle localizedStringForKey:emoticonName
377 table:@"EmoticonNames"];
380 if (localizedEmoticonName)
381 emoticonName = localizedEmoticonName;
384 [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
385 equivalents:[(NSDictionary *)dict objectForKey:EMOTICON_EQUIVALENTS]
393 * @brief Load a Proteus emoticon pack
395 - (void)loadProteusEmoticons:(NSDictionary *)emoticons
397 NSEnumerator *enumerator = [emoticons keyEnumerator];
400 while ((fileName = [enumerator nextObject])) {
401 NSDictionary *dict = [emoticons objectForKey:fileName];
403 [emoticonArray addObject:[AIEmoticon emoticonWithIconPath:[bundle pathForImageResource:fileName]
404 equivalents:[dict objectForKey:@"String Representations"]
405 name:[dict objectForKey:@"Meaning"]
411 * @brief Flush any cached emoticon images (and image attachment strings)
413 - (void)flushEmoticonImageCache
415 NSEnumerator *enumerator;
416 AIEmoticon *emoticon;
418 //Flag our emoticons as enabled/disabled
419 enumerator = [[self emoticons] objectEnumerator];
420 while ((emoticon = [enumerator nextObject])) {
421 [emoticon flushEmoticonImageCache];
426 //Upgrading ------------------------------------------------------------------------------------------------------------
427 //Methods for opening and converting old format Adium emoticon packs
428 #pragma mark Upgrading
430 * @brief Upgrade an emoticon pack from the old format (where every emoticon is a separate file) to the new format
432 - (void)_upgradeEmoticonPack:(NSString *)packPath
434 NSString *packName, *workingDirectory, *tempPackName, *tempPackPath, *fileName;
435 NSDirectoryEnumerator *enumerator;
436 NSFileManager *mgr = [NSFileManager defaultManager];
437 NSMutableDictionary *infoDict = [NSMutableDictionary dictionary];
438 NSMutableDictionary *emoticonDict = [NSMutableDictionary dictionary];
441 packName = [[packPath lastPathComponent] stringByDeletingPathExtension];
442 workingDirectory = [packPath stringByDeletingLastPathComponent];
444 //Rename the existing pack to .AdiumEmoticonOld
445 tempPackName = [packName stringByAppendingPathExtension:EMOTICON_PACK_TEMP_EXTENSION];
446 tempPackPath = [workingDirectory stringByAppendingPathComponent:tempPackName];
447 [mgr movePath:packPath toPath:tempPackPath handler:nil];
449 //Create ourself a new pack
450 [mgr createDirectoryAtPath:packPath attributes:nil];
452 //Version this pack as 1
453 [infoDict setObject:[NSNumber numberWithInt:1] forKey:EMOTICON_PACK_VERSION];
455 //Process all .emoticons in the old pack
456 enumerator = [[NSFileManager defaultManager] enumeratorAtPath:tempPackPath];
457 while ((fileName = [enumerator nextObject])) {
458 if ([[fileName lastPathComponent] characterAtIndex:0] != '.' &&
459 [[fileName pathExtension] caseInsensitiveCompare:EMOTICON_PATH_EXTENSION] == 0) {
460 NSString *emoticonPath = [tempPackPath stringByAppendingPathComponent:fileName];
463 //Ensure that this is a folder and that it is non-empty
464 [mgr fileExistsAtPath:emoticonPath isDirectory:&isDirectory];
466 NSString *emoticonName = [fileName stringByDeletingPathExtension];
468 //Get the text equivalents out of this .emoticon
469 NSArray *emoticonStrings = [self _equivalentsForEmoticonPath:emoticonPath];
471 //Get the image out of this .emoticon
472 NSString *imagePath = [self _imagePathForEmoticonPath:emoticonPath];
473 NSString *imageExtension = [imagePath pathExtension];
475 if (emoticonStrings && imagePath) {
476 NSString *newImageName = [emoticonName stringByAppendingPathExtension:imageExtension];
478 //Move the image into our new pack (with a unique name)
479 NSString *newImagePath = [packPath stringByAppendingPathComponent:newImageName];
480 [mgr copyPath:imagePath toPath:newImagePath handler:nil];
482 //Add to our emoticon plist
483 [emoticonDict setObject:[NSDictionary dictionaryWithObjectsAndKeys:
484 emoticonStrings, EMOTICON_EQUIVALENTS,
485 emoticonName, EMOTICON_NAME, nil]
486 forKey:newImageName];
492 //Write our plist to the new pack
493 [infoDict setObject:emoticonDict forKey:EMOTICON_LIST];
494 [infoDict writeToFile:[packPath stringByAppendingPathComponent:EMOTICON_PLIST_FILENAME] atomically:NO];
496 //Move the old/temp pack to the trash
497 [mgr trashFileAtPath:tempPackPath];
501 * @brief Path to an emoticon image
503 * @param Path within which to search for a file whose name starts with "Emoticon"
505 - (NSString *)_imagePathForEmoticonPath:(NSString *)inPath
507 NSDirectoryEnumerator *enumerator;
510 //Search for the file named Emoticon in our bundle (It can be in any image format)
511 enumerator = [[NSFileManager defaultManager] enumeratorAtPath:inPath];
512 while ((fileName = [enumerator nextObject])) {
513 if ([fileName hasPrefix:@"Emoticon"]) return [inPath stringByAppendingPathComponent:fileName];
520 * @brief Retrieve the text equivalents from a pack
522 - (NSArray *)_equivalentsForEmoticonPath:(NSString *)inPath
524 NSString *equivFilePath = [inPath stringByAppendingPathComponent:@"TextEquivalents.txt"];
525 NSArray *textEquivalents = nil;
527 //Fetch the text equivalents
528 if ([[NSFileManager defaultManager] fileExistsAtPath:equivFilePath]) {
529 NSString *equivString;
531 //Convert the text file into an array of strings
532 equivString = [NSMutableString stringWithContentsOfFile:equivFilePath];
533 equivString = [self _stringWithMacEndlines:equivString];
534 textEquivalents = [[equivString componentsSeparatedByString:@"\r"] retain];
537 return textEquivalents;
541 * @brief Convert any unix/windows line endings to mac line endings
542 * @result The converted string
544 - (NSString *)_stringWithMacEndlines:(NSString *)inString
546 NSCharacterSet *newlineSet = [NSCharacterSet characterSetWithCharactersInString:@"\n"];
547 NSMutableString *newString = nil; //We avoid creating a new string if not necessary
550 //Step through all the invalid endlines
551 charRange = [inString rangeOfCharacterFromSet:newlineSet];
552 while (charRange.length != 0) {
553 if (!newString) newString = [[inString mutableCopy] autorelease];
555 //Replace endline and continue
556 [newString replaceCharactersInRange:charRange withString:@"\r"];
557 charRange = [newString rangeOfCharacterFromSet:newlineSet];
560 return newString ? newString : inString;
563 - (NSString *)description
565 return ([NSString stringWithFormat:@"[%@: %@, ServiceClass %@]",[super description], [self name], [self serviceClass]]);
568 /* Localized emoticon names, listed here for genstrings:
570 AILocalizedStringFromTable(@"Angry", "EmoticonNames", "Emoticon name")
571 AILocalizedStringFromTable(@"Blush", "EmoticonNames", "Emoticon name")
572 AILocalizedStringFromTable(@"Cry", "EmoticonNames", "Emoticon name")
573 AILocalizedStringFromTable(@"Scared", "EmoticonNames", "Emoticon name")
574 AILocalizedStringFromTable(@"Sad", "EmoticonNames", "Emoticon name")
575 AILocalizedStringFromTable(@"Gasp", "EmoticonNames", "Emoticon name")
576 AILocalizedStringFromTable(@"Grin", "EmoticonNames", "Emoticon name")
577 AILocalizedStringFromTable(@"Angel", "EmoticonNames", "Emoticon name")
578 AILocalizedStringFromTable(@"Kiss", "EmoticonNames", "Emoticon name")
579 AILocalizedStringFromTable(@"Lips Are Sealed", "EmoticonNames", "Emoticon name")
580 AILocalizedStringFromTable(@"Money-mouth", "EmoticonNames", "Emoticon name")
581 AILocalizedStringFromTable(@"Smile", "EmoticonNames", "Emoticon name")
582 AILocalizedStringFromTable(@"Sticking Out Tongue", "EmoticonNames", "Emoticon name")
583 AILocalizedStringFromTable(@"Erm", "EmoticonNames", "Emoticon name")
584 AILocalizedStringFromTable(@"Cool", "EmoticonNames", "Emoticon name")
585 AILocalizedStringFromTable(@"Wink", "EmoticonNames", "Emoticon name")
586 AILocalizedStringFromTable(@"Foot In Mouth", "EmoticonNames", "Emoticon name")
587 AILocalizedStringFromTable(@"Frown", "EmoticonNames", "Emoticon name")
588 AILocalizedStringFromTable(@"Confused", "EmoticonNames", "Emoticon name")
589 AILocalizedStringFromTable(@"Halo", "EmoticonNames", "Emoticon name")
590 AILocalizedStringFromTable(@"Undecided", "EmoticonNames", "Emoticon name")
591 AILocalizedStringFromTable(@"Embarrassed", "EmoticonNames", "Emoticon name")